#include "ET++.ph"
#ifdef __GNUG__
#pragma implementation
#endif

#include "CodeTextView.h"

#include "Class.h"
#include "VObjectText.h"
#include "RunArray.h"
#include "Env.h"
#include "Math.h"
#include "WindowSystem.h"
#include "Timer.h"
#include "System.h"
#include "FileData.h"
#include "Picture.h"
#include "TextMarks.h"
#include "RegularExp.h"
#include "TextReader.h"
#include "String.h"

static const char
	*match     = "lass",
	*obrackets = "({[\"",
	*cbrackets = ")}]\"";

static Ink
	*commentInk,
	*functionInk,
	*classInk;

static struct regExps_t {
	char *rex;
	RegularExp *r;
	int tag;        // register to put into VisualMark
	int file;       // register used as a filename
	int pre;        // register to put in front of mark
	int post;       // register to put at the end of mark
} regExps[] = {
	{ "^\\(#[ \t]*include[ \t]*\"\\)\\(.+\\.xpm3?\\|.+\\.image\\|.+\\.icon\\)\\(\"\\)",
												0,  2, 2, 1, 3 },
	{ "\\(//@Pict:\"\\)\\([^\"]+\\)\\(\"\\)",       0,  0, 2, -1, -1 },
	{ 0,                                                                            0,  -1, -1, -1, -1 }
};

ONEXIT(CodeTextView)
{
	for (int i= 0; regExps[i].rex; i++)
		SafeDelete(regExps[i].r);
}

//---- CodeReformatter ---------------------------------------------------------

class CodeReformatter : public Timer {
	CodeTextView *textView;
public:
	CodeReformatter(CodeTextView *ct, int ms) : Timer(ms)
		{ textView= ct; }
	bool Notify();
};

bool CodeReformatter::Notify()
{
	if (textView->NeedsReformat())
		textView->Reformat();
	Reset();
	return TRUE;
}

//---- CodeTextView ------------------------------------------------------------

NewMetaImpl(CodeTextView,VObjectTextView, (TB(autoIndent), T(cursorPos)));

CodeTextView::CodeTextView(EvtHandler *eh, const Rectangle &r, VObjectText *t,
		bool w, TextViewFlags f, const Point &b, TViewAlign ta, int id)
				: VObjectTextView(eh, r, t, w, f, b, ta, id)
{
	SetStopChars("\n\r");
	autoIndent= TRUE;
	SetupStyles(t->GetFont());
	ExitCursorMode();
	int interval= Env::GetValue("CodeText.AutoReformatInterval", 10000);
	if (interval > 0)
		reformatter= new CodeReformatter(this, interval);
	else
		reformatter= 0;
}

CodeTextView::~CodeTextView()
{
	if (reformatter) {
		reformatter->Remove();
		reformatter= 0;
	}
}

Command *CodeTextView::DoKeyCommand(int ch, Token token)
{
	ExitCursorMode();
	if (autoIndent && (ch == '\r' || ch == '\n')) {     // auto indent
		int from, to, i;
		Text *t= GetText(),
		     *scratchText= text->MakeScratchText(0, 20);
		GetSelection(&from, &to);
		scratchText->Append('\n');
		for (i= StartLine(CharToLine(from)); i < t->End(); i++) {
			ch= (*t)[i];
			if (ch == ' ' || ch == '\t')
				scratchText->Append(ch);
			else
				break;
		}
		Command *cmd= InsertText(scratchText);
		delete scratchText;
		RevealSelection();
		return cmd;
	}
	return TextView::DoKeyCommand(ch, token);
}

void CodeTextView::SetFont(Font *fp)
{
	SetupStyles(fp);
	TextView::SetFont(fp);
}

void CodeTextView::SetupStyles(Font *fp)
{
	int declsize= fp->Size()
		+ Env::GetValue("CodeText.DeclarationSizeIncrement", 2);
	if (commentInk == 0) {
		if (gColor) {
			commentInk= new_Ink("CodeText.CommentColor", new_RGBColor(0, 0, 255));
			functionInk= new_Ink("CodeText.FunctionColor", new_RGBColor(0, 0, 0));
			classInk= new_Ink("CodeText.ClassColor", new_RGBColor(0, 0, 0));
		} else
			commentInk= functionInk= classInk= gInkBlack;
	}

	commentStyle= new_CharStyle(new_Font(fp->Fid(), fp->Size(), eFaceItalic), commentInk);
	functionStyle= new_CharStyle(new_Font(fp->Fid(), declsize, eFaceBold), functionInk);
	classDeclStyle= new_CharStyle(new_Font(fp->Fid(), declsize, eFaceBold), classInk);
	plainStyle= new_CharStyle(new_Font(fp->Fid(), fp->Size(), eFacePlain));
}

Command *CodeTextView::DoLeftButtonDownCommand(Point p, Token t, int cl)
{
	Point sp(p);
	ExitCursorMode();
	if (!Enabled())
		return gNoChanges;
	if (cl >= 2) {
		int line, cpos;
		char *br;
		Point pp;
		Text *txt= GetText();

		p-= GetInnerOrigin();
		PointToPoint(p, &pp, &line, &cpos);
		
		if (cpos > 0 && (br= strchr(obrackets,(*txt)[cpos-1]))) {
			MatchBracketForward(cpos, (int)*br, cbrackets[br-obrackets]);
			return gNoChanges;
		}
		if (cpos < txt->End() && (br= strchr(cbrackets,(*txt)[cpos]))) {
			MatchBracketBackward(cpos-1, obrackets[br-cbrackets], (int)*br);
			return gNoChanges;
		}
	}
	return TextView::DoLeftButtonDownCommand(sp, t, cl);
}

Command *CodeTextView::DoMenuCommand(int m)
{
	ExitCursorMode();
	return TextView::DoMenuCommand(m);
}

Command *CodeTextView::DoOtherEventCommand(Point p, Token t)
{
	ExitCursorMode();
	return TextView::DoOtherEventCommand(p, t);
}

void CodeTextView::MatchBracketForward(int from, int obracket, int cbracket)
{
	Text *txt= GetText();
	int ch, stack= 0;

	for (int i= from; i < txt->End(); i++) {
		ch= (*txt)[i];
		if (ch == cbracket) {
			if (stack-- == 0)
				break;
		} else if (ch == obracket)
			stack++;
	}
	SetSelection(from, i, TRUE);
}

void CodeTextView::MatchBracketBackward(int from, int obracket, int cbracket)
{
	Text *txt= GetText();
	int ch, stack= 0;

	for (int i= from; i >= 0; i--) {
		ch= (*txt)[i];
		if (ch == obracket) {
			if (stack-- == 0)
				break;
		} else if (ch == cbracket)
			stack++;
	}
	SetSelection(i+1, from+1, TRUE);
}

Command *CodeTextView::DoCursorKeyCommand(EvtCursorDir cd, Token t)
{
	if (cd == eCrsLeft || cd == eCrsRight)
		ExitCursorMode();
	return TextView::DoCursorKeyCommand(cd, t);
}

int CodeTextView::CursorPos(int at, int line, EvtCursorDir d, Point p)
{
	int charNo;
	Point basePoint;

	if (cursorPos == gPoint_1)
		CharToPoint(at, &line, &cursorPos);

	if (d == eCrsDown)
		line= Math::Min(nLines-1, line+1);
	else
		line= Math::Max(0, line-1);
	basePoint= LineToPoint(line, TRUE) + Point(cursorPos.x, 0);
	PointToPoint(basePoint, &p, &line, &charNo);
	return charNo;
}

PrettyPrinter *CodeTextView::MakePrettyPrinter(Text *t, CharStyle *cs,
				CharStyle *fs, CharStyle *cds, CharStyle *ps)
{
	return new PrettyPrinter(t, cs, fs, cds, ps);
}

void CodeTextView::FormatCode()
{
	if (!text->IsKindOf(StyledText))
		return;
	PrettyPrinter *pp= MakePrettyPrinter(text, commentStyle, functionStyle,
						classDeclStyle, plainStyle);
	pp->Doit();
	SafeDelete(pp);
	needsReformat= FALSE;
}

void CodeTextView::SetDefaultStyle()
{
	if (!text->IsKindOf(StyledText))
		return;
	StyledText *stext= (StyledText*)text;
	RunArray *st= new RunArray();
	st->Insert(plainStyle, 0, 0, stext->Size());
	RunArray *tmpp= stext->SetCharStyles(st);
	delete tmpp;
}

void CodeTextView::DoObserve(int id, int part, void *what, Object *op)
{
	TextView::DoObserve(id, part, what, op);
	if (op == GetText() && (part == eTextDeleted || eTextReplaced))
		InvalidateFormatting();
}

void CodeTextView::InvalidateFormatting()
{
	needsReformat= TRUE;
	if (reformatter)
		reformatter->Reset();
}

void CodeTextView::Reformat()
{
	FormatCode();
	if (NumberOfLines() > 500) // cheat
		ForceRedraw();
	else
		RepairAll();
}

Command *CodeTextView::PasteData(Data *data)
{
	if (data->IsKindOf(FileData)
			&& Env::GetValue("CodeText.AllowGraphics", TRUE)) {
		if (data->CanConvert(Meta(VObject))
			|| data->CanConvert(Meta(Picture))
				|| data->CanConvert(Meta(Bitmap)))
			return new PasteVObjectCommand(this,
				new ReferenceMark(data->FullName(),
					form("//@Pict:\"%s\"", data->FullName())));
	}
	return TextView::PasteData(data);
}

//---- ???? --------------------------------------------------------------------

bool ReadFromAsPureTextWithMarks(IStream &s, long sizehint, StyledText *tx)
{
	if (!Env::GetValue("CodeText.AllowGraphics", TRUE)) {
		tx->ReadFromAsPureText(s, sizehint);
		return TRUE;
	}

	int size= Env::GetValue("filebrowser.FontSize", 12);
	Font *fd= new_Font(gFixedFont->Fid(), size, eFacePlain);
	TextReader tr(tx, new_CharStyle(fd), new_ParaStyle());
	char buf[1000];
	int len, pos;
	RegExRegs regs;
	regExps_t *t;

	while (s.getline(buf, 1000)) {
		for (t= regExps; t->rex; t++) {
			if (t->r == 0)
				t->r= new RegularExp(t->rex, cCaseSensitive);
			pos= t->r->SearchForward(buf, &len, 0, -1, cMaxInt, &regs);
			if (pos >= 0)
				break;
		}
		if (pos < 0) {
			tr.Append(buf);
		} else {
			tr.Append(buf, pos);
			tr.Append(regs.StringAt(buf, t->pre));
			VisualMark *vm= new ReferenceMark(regs.StringAt(buf, t->file));
			vm->SetTag(regs.StringAt(buf, t->tag));
			tr.InsertVisualMark(vm);
			tr.Append(regs.StringAt(buf, t->post));
			tr.Append(&buf[pos+len]);
		}
	}
	return TRUE;
}

bool PrintOnAsPureTextWithMarks(OStream &s, StyledText *tx)
{
	// ??? terminate last line with a '\n' ???
	//if ((*tx)[tx->Size()-1] != '\n') {
	//       fprintf(stderr, "appending newline\n");
	//        tx->Append('\n');
	//}

	if (!Env::GetValue("CodeText.AllowGraphics", TRUE))
		tx->PrintOnAsPureText(s);
	else {
		byte escape= tx->GetMarkChar();
		AutoTextIter next(tx);
		int c;
		while ((c= next()) != cEOT) {
			if (c == escape && tx->IsVisualMark(next->GetLastPos()))
				s.form("%s", tx->GetVisualMarkAt(next->GetLastPos())->GetTag());
			else if (c)
				s.put((char)c);
		}
	}
	return TRUE;
}

//---- CodeAnalyzer ------------------------------------------------------------

CodeAnalyzer::CodeAnalyzer(Text *t)
{
	text= t;
}

void CodeAnalyzer::Doit()
{
	inDefine= isescape= inClass= FALSE;
	lastComment= prevCh= braceStack= inString= line= 0;
	c= '\n';
	int canBeClass= canBeFunction= -1;

	AutoTextIter next(text, 0, text->Size());
	Start();

	while ((c= next()) != cEOT) {
		if (isescape)
			isescape= FALSE;
		else
			switch (c) {
			case '#':
				if (prevCh == '\n')
					inDefine= TRUE;
				break;
			case '\\':
				isescape= TRUE;
				break;
			case '\n':
				inDefine= FALSE;
				line++;
				break;
			case '*':
				if (prevCh == '/' && inString == 0) {
					if (canBeFunction == -1 && canBeClass == -1)
						lastComment= next->GetPos();
					FoundComment(&next);
					prevCh= 0;
					continue;
				}
				break;
			case '/':
				if (prevCh == '/' && inString == 0) {
					if (canBeFunction == -1 && canBeClass == -1)
						lastComment= next->GetPos();
					FoundEndOfLineComment(&next);
					prevCh= 0;
					continue;
				}
				break;
			case ';':
				canBeFunction= canBeClass= -1;
				break;
			case '{':
				if (canBeFunction != -1) {
					FoundFunctionOrMethod(canBeFunction, lastComment);
					canBeFunction= -1;
				}
				if (canBeClass != -1) {
					FoundClassDecl(canBeClass);
					canBeClass= -1;
				}
				if (inString == 0)
					braceStack++;
				break;
			case '}':
				if (inString == 0)
					braceStack--;
				break;
			case '(':
				if (!inDefine && inString == 0) {
					if (braceStack == 0 && canBeFunction == -1)
						canBeFunction= next->GetPos()-1;
					braceStack++;
				}
				break;
			case ')':
				if (!inDefine && inString == 0)
					braceStack--;
				break;
			case '\'':
			case '\"':
				if (inString == 0) {
					if (c == '\"')
						inString= '\"';
					else
						inString= '\'';
				} else {
					if ((inString == '\"' && c == '\"')
					 || (inString == '\'' && c == '\''))
						inString= 0;
				}
				break;
			default:
				if (c == 'c' && !inDefine && inString == 0
					&& !inClass && canBeClass == -1) {
					if (braceStack == 0)
						if (inClass= IsClassDecl(next->GetPos()))
							next->SetPos(next->GetPos()+4);
				} else if (inClass && Isinword(c)) {
					inClass= FALSE;
					canBeClass= next->GetPos();
				}
				break;
		}
		prevCh= c;
	}
	End();
}

void CodeAnalyzer::FoundComment(AutoTextIter *next)
{
	int c, prevCh= 0, l= line, start= (*next)->GetPos()-2;

	while ((c= (*next)()) != cEOT) {
		if (c == '\n')
			line++;
		if (c == '/' && prevCh == '*') {
			Comment(l, start, (*next)->GetPos());
			break;
		}
		prevCh= c;
	}
}

void CodeAnalyzer::FoundEndOfLineComment(AutoTextIter *next)
{
	int c, start= (*next)->GetPos()-2;

	while ((c= (*next)()) != cEOT)
		if (c == '\n') {
			Comment(line, start, (*next)->GetPos()-1);
			line++;
			break;
		}
}

void CodeAnalyzer::FoundFunctionOrMethod(int at, int lastComment)
{
	byte c;
	int pos= at, len= 0;
	while (--pos >= lastComment) {
		c= (*text)[pos];
		if (!Isspace(c) && !strchr("[]*+-=%&><|:^%()~/",c))
						// overloaded operators
			break;
	}
	while (pos >= lastComment) {
		c= (*text)[pos];
		if (!(Isinword(c) || c == ':' || c == '~' ))
			break;
		if (pos-1 < 0)
			break;
		pos--;
		len++;
	}

	if (len) {
		char buf[500];
		pos++;
		text->CopyInStr((byte*)buf, sizeof buf, pos, pos+len);
		char *p= strchr(buf, ':');
		if (!p)
			Function(line, pos, pos+len, buf, 0);
		else {
			*p= '\0';
			char *pp= p+2;
			Function(line, pos, pos+len, pp, buf);
		}
	}
}

bool CodeAnalyzer::IsClassDecl(int at)
{
	const char *p= match;
	int c;
	AutoTextIter next(text, at, text->Size());

	while ((c= next()) != cEOT && *p)
		if (c != *p++)
			return FALSE;
	return TRUE;
}

void CodeAnalyzer::FoundClassDecl(int at)
{
	int end, c, start= at-1;
	AutoTextIter next(text, start, text->Size());

	while ((c= next()) != cEOT)
		if (!Isinword(c))
			break;
	end= next->GetPos()-1;
	if (start != end) {
		static byte name[1000];
		text->CopyInStr(name, sizeof name, start, end);
		ClassDecl(line, start, end, (char*)name);
	}
}

void CodeAnalyzer::Start()
{
}

void CodeAnalyzer::End()
{
}

void CodeAnalyzer::Comment(int, int, int)
{
}

void CodeAnalyzer::ClassDecl(int, int, int, const char*)
{
}

void CodeAnalyzer::Function(int, int, int, const char*, const char*)
{
}

//---- PrettyPrinter -----------------------------------------------------------

PrettyPrinter::PrettyPrinter(Text *t, CharStyle *cs, CharStyle *fs,
				CharStyle *cds, CharStyle *ps) : CodeAnalyzer(t)
{
	stext= Guard(t, StyledText);
	plainStyle= ps;
	commentStyle= cs;
	classDeclStyle= cds;
	functionStyle= fs;
}

void PrettyPrinter::Start()
{
	st= new RunArray();
	st->Insert(plainStyle, 0, 0, stext->Size());
}

void PrettyPrinter::End()
{
	RunArray *tmpp= stext->SetCharStyles(st);
	delete tmpp;
}

void PrettyPrinter::Comment(int, int start, int end)
{
	st->Insert(commentStyle, start, end, end-start);
}

void PrettyPrinter::ClassDecl(int, int start, int end, const char*)
{
	st->Insert(classDeclStyle, start, end, end-start);
}

void PrettyPrinter::Function(int, int start, int end, const char*, const char*)
{
	st->Insert(functionStyle, start, end, end-start);
}

